/**
 * 
 */
package ro.dta.idbi.model;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

import ro.dta.idbi.interfaces.IMemento;
import ro.dta.idbi.interfaces.IProject;
import ro.dta.idbi.interfaces.IntegrationOperation;
import uk.ac.ic.doc.automed.IntegrityException;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.reps.Schema;

import com.mxgraph.io.mxCodec;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.util.mxCellRenderer;
import com.mxgraph.view.mxGraph;

/**
 * Integration project model
 * 
 * @author Tudor Dobrila
 * 
 */
public class IntegrationProject extends Observable implements IProject {

	/**
	 * Path to XML file containing this project
	 */
	protected String fileName;

	/**
	 * Steps in the integration process
	 */
	protected transient List<List<String>> steps;

	/**
	 * Name of the schemas resulting after each integration step
	 */
	protected transient List<String> interSchemas;

	/**
	 * Current step in the integration process
	 */
	protected transient int currentStep;

	/**
	 * Operation currently performed in the integration project at current step
	 */
	protected transient IntegrationOperation currentOperation;

	/**
	 * Items being integrated at current step
	 */
	protected transient List<String> crtStepItems;

	/**
	 * Schema transformation history for the current step, used to undo
	 * transformations
	 */
	protected transient List<List<Memento>> history;

	/**
	 * Integration strategy model
	 */
	protected mxGraphModel strategy;

	/**
	 * Cached strategy image
	 */
	protected transient BufferedImage cachedStrategy;

	/**
	 * True if the project has changed, false otherwise<br />
	 * 
	 * Used to re-compute a cache of the strategy image when the project has
	 * changed
	 */
	protected transient boolean hasChanged;

	/**
	 * Integration project class constructor with empty integration strategy
	 * 
	 */
	public IntegrationProject() {
		super();
		this.strategy = new mxGraphModel();
		this.steps = new ArrayList<List<String>>();
		this.currentStep = -1;
		this.currentOperation = IntegrationOperation.CONFORMING;
	}

	/**
	 * Integration project class constructor
	 * 
	 * @param strategyModel
	 *            Strategy graph model that defines the integration strategy
	 *            chosen by the user
	 * @param currentStep
	 *            Current step in the integration process
	 * @param crtOperation
	 *            Current operation performed in the integration process
	 * @param crtStepItems
	 *            Current items being integrated in the current step
	 */
	public IntegrationProject(final mxGraphModel strategyModel, final int currentStep,
			final IntegrationOperation crtOperation, final List<String> crtStepItems) {
		super();
		this.strategy = strategyModel;
		this.currentStep = currentStep;
		this.crtStepItems = crtStepItems;
		this.currentOperation = crtOperation;
		this.history = new ArrayList<List<Memento>>();
		strategyToSteps();
		resetHistory();
	}

	/**
	 * Integration project class construct
	 * 
	 * @param fileName
	 *            XML file containing the project to be loaded
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws SAXException
	 * @throws ProjectLoadException
	 */
	public IntegrationProject(final String fileName) throws ParserConfigurationException,
			SAXException, IOException, ProjectLoadException {
		super();
		this.fileName = fileName;
		final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		final DocumentBuilder builder = factory.newDocumentBuilder();
		final Document doc = builder.parse(fileName);
		NodeList list;

		// Parse current step
		list = doc.getElementsByTagName("currentStep");
		getCurrentStep(list);

		// Parse current operation
		list = doc.getElementsByTagName("currentOperation");
		getCurrentOperation(list);

		// Parse strategy graph model
		list = doc.getElementsByTagName("mxGraphModel");
		getStrategy(list);

		// Parse current items
		list = doc.getElementsByTagName("currentItems");
		getCurrentItems(list);
	}

	/**
	 * Extract the current step from a node list
	 * 
	 * @param list
	 *            Node list containing the current step
	 */
	private void getCurrentStep(final NodeList list) throws ProjectLoadException {
		if (list != null && list.getLength() == 1) {
			final Node node = list.item(0);
			currentStep = Integer.parseInt(node.getFirstChild().getTextContent());
		} else {
			throw new ProjectLoadException(
					"Could not parse the project file. Invalid <currentStep> tag.");
		}
	}

	/**
	 * Extract the current operation from a node list
	 * 
	 * @param list
	 *            Node list containing the current operation
	 */
	private void getCurrentOperation(final NodeList list) throws ProjectLoadException {
		if (list != null && list.getLength() == 1) {
			final Node node = list.item(0);
			currentOperation = IntegrationOperation.valueOf(node.getFirstChild().getTextContent());
		} else {
			throw new ProjectLoadException(
					"Could not parse the project file. Invalid <currentOperation> tag.");
		}
	}

	/**
	 * Extract the strategy from a node list
	 * 
	 * @param list
	 *            Node list containing the strategy
	 */
	private void getStrategy(final NodeList list) throws ProjectLoadException {
		if (list != null && list.getLength() == 1) {
			final mxCodec codec = new mxCodec();
			strategy = new mxGraphModel();
			codec.decode(list.item(0), strategy);
			strategyToSteps();
		} else {
			throw new ProjectLoadException(
					"Could not parse the project file. Invalid <strategy> tag.");
		}
	}

	/**
	 * Extract the current step items from a node list
	 * 
	 * @param list
	 *            Node list containing the current step items
	 */
	private void getCurrentItems(final NodeList list) throws ProjectLoadException {
		if (list != null && list.getLength() == 1) {
			crtStepItems = new ArrayList<String>();
			final Node node = list.item(0);
			final NodeList childs = node.getChildNodes();

			for (int i = 0; i < childs.getLength(); i++) {
				final Node node1 = childs.item(i);

				if (node1.getNodeName().equals("item")) {
					crtStepItems.add(node1.getFirstChild().getTextContent());
				}
			}

			resetHistory();
		} else {
			throw new ProjectLoadException(
					"Could not parse the project file. Invalid <currentItems> tag.");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ro.dta.idbi.model.IProject#save()
	 */
	@Override
	public void save() throws TransformerException, ParserConfigurationException, IOException {
		if (fileName != null && !fileName.equals("")) {
			saveAs(fileName);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ro.dta.idbi.model.IProject#saveAs(java.lang.String)
	 */
	@Override
	public void saveAs(final String fileName) throws TransformerException,
			ParserConfigurationException, IOException {
		Document xmlDoc;
		final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		final DocumentBuilder builder = factory.newDocumentBuilder();
		final DOMImplementation impl = builder.getDOMImplementation();
		Element element;
		Node node;

		xmlDoc = impl.createDocument(null, "idbiproj", null);
		final Element root = xmlDoc.getDocumentElement();

		// Current step
		element = xmlDoc.createElement("currentStep");
		node = xmlDoc.createTextNode(Integer.toString(currentStep));
		element.appendChild(node);
		root.appendChild(element);

		// Current operation
		element = xmlDoc.createElement("currentOperation");
		node = xmlDoc.createTextNode(currentOperation.toString());
		element.appendChild(node);
		root.appendChild(element);

		// Current step items
		element = xmlDoc.createElement("currentItems");
		if (crtStepItems != null) {

			for (String i : crtStepItems) {
				final Element element1 = xmlDoc.createElement("item");
				node = xmlDoc.createTextNode(i);
				element1.appendChild(node);
				element.appendChild(element1);
			}
		}
		root.appendChild(element);

		// Strategy
		if (strategy != null) {
			element = xmlDoc.createElement("strategy");
			final mxCodec codec = new mxCodec();
			node = xmlDoc.importNode(codec.encode(strategy), true);
			element.appendChild(node);
			root.appendChild(element);
		}

		final DOMSource domSource = new DOMSource(xmlDoc);
		final FileOutputStream out = new FileOutputStream(fileName);
		final StreamResult streamResult = new StreamResult(out);
		final TransformerFactory trans = TransformerFactory.newInstance();
		final Transformer serializer = trans.newTransformer();
		serializer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
		serializer.setOutputProperty(OutputKeys.INDENT, "yes");
		serializer.transform(domSource, streamResult);
		out.close();

		// Set the new save location for this file
		this.fileName = fileName;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ro.dta.idbi.model.IProject#getFileName()
	 */
	@Override
	public String getFileName() {
		return fileName;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ro.dta.idbi.model.IProject#setFileName(java.lang.String)
	 */
	@Override
	public void setFileName(final String fileName) {
		this.fileName = fileName;
		notifyAll();
	}

	/**
	 * Convert the strategy model to the steps map
	 */
	private void strategyToSteps() {
		steps = new ArrayList<List<String>>();
		interSchemas = new ArrayList<String>();
		final Collection<Object> cells = strategy.getCells().values();
		final List<mxCell> roots = new ArrayList<mxCell>();

		// Find the roots of the trees in the forest
		for (Object o : cells) {
			final mxCell cell = (mxCell) o;

			if (cell.isVertex() && mxGraphModel.getOutgoingEdges(strategy, cell).length == 0) {
				roots.add(cell);
			}
		}

		// Perfrom BFS on each tree in the forest
		final LinkedList<mxCell> queue = new LinkedList<mxCell>();
		for (mxCell root : roots) {
			// Select the root of a tree
			queue.clear();
			queue.push(root);

			// Perform BFS on current tree
			while (!queue.isEmpty()) {
				final mxCell crtCell = queue.pop();
				final Object[] childrenEdges = mxGraphModel.getIncomingEdges(strategy, crtCell);
				if (childrenEdges.length > 0) {
					final List<String> crtStep = new ArrayList<String>();

					for (Object o : childrenEdges) {
						final mxCell crtEdge = (mxCell) o;
						final mxCell crtChild = (mxCell) crtEdge.getSource();
						final String value = crtChild.getValue().toString();

						if (!crtStep.contains(value)) {
							crtStep.add(value);
						}
						queue.push(crtChild);

					}

					steps.add(0, crtStep);
					interSchemas.add(0, crtCell.getValue().toString());
				}
			}
		}
	}

	@Override
	public mxGraphModel getStrategy() {
		return strategy;
	}

	@Override
	public int getCurrentStep() {
		return currentStep;
	}

	@Override
	public void incCurrentStep() {
		currentStep++;
	}

	@Override
	public void advance() throws NotFoundException, IntegrityException {
		if (currentStep == -1) {
			// Done with strategy definition phase, start integration phase
			currentStep = 0;
			currentOperation = IntegrationOperation.CONFORMING;
			crtStepItems = new ArrayList<String>();

			for (String s : steps.get(currentStep)) {
				crtStepItems.add(s);
			}
		} else {
			if (currentOperation == IntegrationOperation.IMPROVEMENT) {
				// If we've just integrated a set of schemas, proceed to the
				// next set of schemas
				final Schema finalSch = Schema.getSchema(crtStepItems.get(0));
				finalSch.setName(interSchemas.get(currentStep));
				currentStep++;
				
				if (currentStep < steps.size()) {
					// There is another step that needs to be executed
					crtStepItems.clear();
					for (String s : steps.get(currentStep)) {
						crtStepItems.add(s);
					}

					currentOperation = IntegrationOperation.CONFORMING;
				} else {
					return;
				}
			} else if (currentOperation == IntegrationOperation.CONFORMING) {
				// Finished conforming, start merging phase for current set of
				// schemas
				currentOperation = IntegrationOperation.MERGING;
				// Merge conformed schemas
				final Schema result = AutoMedUtils.mergeSchemas(crtStepItems);
				crtStepItems.clear();
				crtStepItems.add(result.getName());
			} else if (currentOperation == IntegrationOperation.MERGING) {
				// Finished merging, start improvement phase for current set of
				// schemas
				currentOperation = IntegrationOperation.IMPROVEMENT;
			}
		}

		resetHistory();
		hasChanged = true;
	}

	@Override
	public boolean canAdvance() {
		return currentStep != steps.size();
	}

	@Override
	public boolean isLastStep() {
		return currentStep == (steps.size() - 1)
				&& currentOperation == IntegrationOperation.IMPROVEMENT;
	}

	@Override
	public List<String> getSchemasForCurrentStep() {
		return crtStepItems;
	}

	@Override
	public List<String> getSchemas(final int step, final IntegrationOperation operation) {
		List<String> res;
		if (operation == IntegrationOperation.CONFORMING) {
			res = steps.get(step);
		} else {
			res = new ArrayList<String>();
			res.add(interSchemas.get(step));
		}

		return res;
	}

	@Override
	public List<String> getSchemas(final int step) {
		return steps.get(step);
	}

	@Override
	public IntegrationOperation getCurrentOperation() {
		return currentOperation;
	}

	@Override
	public void setStrategy(final mxGraphModel strategy) {
		if (currentStep == -1) {
			this.strategy = strategy;
			strategyToSteps();
		}
	}

	@Override
	public BufferedImage getStrategyImage() {
		BufferedImage result;
		int crtStep = currentStep == steps.size() ? currentStep - 1 : currentStep;
		
		if (cachedStrategy == null || hasChanged) {
			final Object[] cells = strategy.cloneCells(new Object[] { strategy.getRoot() }, true);
			final mxGraph graph = new mxGraph();
			graph.addCells(cells);

			for (Object o : ((mxGraphModel) graph.getModel()).getCells().values()) {
				final mxCell crtCell = (mxCell) o;

				if (crtCell.isVertex()) {
					if (steps.get(crtStep).contains(crtCell.getValue())) {
						crtCell.setStyle("ROUNDED;strokeColor=black;fillColor=#5fc40d");
					}

					if (interSchemas.get(crtStep).equals(crtCell.getValue())) {
						crtCell.setStyle("ROUNDED;strokeColor=black;fillColor=#961919;opacity=50");
					}
				}
			}

			final BufferedImage image = mxCellRenderer.createBufferedImage(graph, null, 1,
					Color.WHITE, true, null);
			hasChanged = false;
			result = image;
		} else {
			result = cachedStrategy;
		}

		return result;
	}

	@Override
	public void replaceCrtStepItem(final String oldSchema, final String newSchema,
			final String command) {
		int oldIndex = crtStepItems.indexOf(oldSchema);

		if (oldIndex != -1) {
			crtStepItems.set(oldIndex, newSchema);
			history.get(oldIndex).add(new Memento(oldSchema, command));
		}
	}

	@Override
	public IMemento undoSchema(String schema) throws NotFoundException, IntegrityException {
		int oldIndex = crtStepItems.indexOf(schema);

		// Can revert to a previous schema
		if (oldIndex != -1) {
			List<Memento> crtHistory = history.get(oldIndex);
			if (crtHistory.size() == 0) {
				return null;
			}

			IMemento lastSchema = crtHistory.get(crtHistory.size() - 1);
			Schema source = Schema.getSchema(lastSchema.getOriginalSchema());
			Schema target = Schema.getSchema(schema);

			Schema[] interSch = source.findShortestPath(target);
			interSch[1].oldRetract();

			crtHistory.remove(lastSchema);
			crtStepItems.set(oldIndex, lastSchema.getOriginalSchema());

			return lastSchema;
		}

		return null;
	}

	@Override
	public boolean canUndo(String schema) {
		int oldIndex = crtStepItems.indexOf(schema);

		// Can revert to a previous schema
		if (oldIndex != -1 && history.get(oldIndex).size() > 0) {
			return true;
		}

		return false;
	}

	/**
	 * Reset the history for the current step
	 */
	private void resetHistory() {
		this.history = new ArrayList<List<Memento>>();
		for (int i = 0; i < crtStepItems.size(); i++) {
			this.history.add(new ArrayList<Memento>());
		}
	}
}
